package hardware;

import java.util.Arrays;

public class L3Cache {
	private String writePolicy;
	private int cycles;
	private int m, s, l;
	private CacheEntry[][] cache;
	private L1Cache l1;
	private L2Cache l2;
	private int memAddSize;
	private int disp;
	final static String NOT_FOUND = "not found l3";
	final static String WRITE_BACK = "Write Back";
	final static String WRITE_THROUGH = "Write Through";
	
	static int hit, accessTimes = 1;
	static int miss = 1;
	
	public L3Cache(String wp, int c, int s, int l, int m) {
		writePolicy = wp;
		setCycles(c);
		this.s = s;
		this.l = l;
		this.m = m;
		this.disp = (Log.log(l / 16));
		cache = new CacheEntry[s / l][l/16];
	}

	/**
	 * Read hit from L3 Cache, throws exception NOT_FOUND if not found in L3
	 * Cache, to be handled then by the Cache class to perform cache miss on L
	 * 
	 * @param address
	 *            the address to read from
	 * @param currentTime
	 *            the current time at which the operation is performed.
	 * @return the value from the cache
	 * @throws Exception
	 *             if the the address is not found in the cache.
	 */
	public int readFromCache(int address, long currentTime) throws Exception {
		accessTimes++;
		int baseAddress = (int) (address - address%Math.pow(2, disp));
		for (int i = 0; i < cache.length; i++) {
			hit++;
			if (cache[i] != null) {
				if (cache[i][0] != null && cache[i][0].check(baseAddress)) { //Found
					for(int j=0; j<cache[i].length; j++){
							cache[i][j].setTimeInCache(currentTime);
					}
					l2.updateFromL3Cache(address ,baseAddress, cache[i], currentTime);
					return cache[i][address-baseAddress].getValue();
				}
			}
		}
		miss++;
		throw new Exception(NOT_FOUND, new Throwable());
	}
	
	/**
	 * Read hit from L3 Cache, throws exception NOT_FOUND if not found in L3
	 * Cache, to be handled then by the Cache class to perform cache miss on L3
	 * 
	 * @param address
	 *            the address to read from
	 * @param currentTime
	 *            the current time at which the operation is performed.
	 * @return the instruction from the cache
	 * @throws Exception
	 *             if the the address is not found in the cache.
	 */
	public Instruction readFromICache(int address, long currentTime) throws Exception {
		accessTimes++;
		int baseAdress = (int) (address - address%Math.pow(2, disp));
		for (int i = 0; i < cache.length; i++) {
			if (cache[i] != null) {
				if (cache[i][0] != null && cache[i][0].check(baseAdress)) { //Found
					for(int j=0; j<cache[i].length; j++){
							cache[i][j].setTimeInCache(currentTime);
					}
					hit++;
					return cache[i][address-baseAdress].getInstruction();
				}
			}
		}
		miss++;
		throw new Exception(NOT_FOUND, new Throwable());
	}
	
	/**
	 * Updates the cache with the given address by the given value.
	 * @param address the address to update the value in.
	 * @param value the value to update the cache with.
	 * @param currentTime the current time at which the operation is performed.
	 * @return boolean true or false to indicate whether the address is found in the cache or not.
	 */
	public boolean updateValue(int address, int value, long currentTime){
		int baseAddress = (int) (address - address%Math.pow(2, disp)); //The base address is modified in the code below 
		for(int i = 0; i<cache.length; i++){
			if(cache[i][0]!=null)
			if(cache[i][0].getAddress() == baseAddress){ 
				int index = address - baseAddress;
				cache[i][index].setValue(value) ;
				cache[i][0].setTimeInCache(currentTime);
				cache[i][index].setTimeInCache(currentTime);
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Read miss in L3 Cache, which get the value from the memory and updates
	 * the L3 Cache (This is called only if one level cache).
	 * 
	 * @param address
	 *            the address of the value to be read from the memory.
	 * @param currentTime
	 *            the time at which the operation is performed
	 * @return the value read from the memory.
	 */
	public int readMiss(int address, long currentTime) {
		int baseAddress = (int) (address - address%Math.pow(2, disp)); //The base address is modified in the code below
		int [] value = new int[l/16];
		for(int i=0; i<value.length; i++)
			value[i] = Memory.load(baseAddress+i);
		int temp = baseAddress; //Stored in temp value to be used as the original base address
		if (m == 1) {
			int disp = (Log.log(l / 16));
			baseAddress = (int) (baseAddress / Math.pow(2, disp));
			int index = baseAddress % (s / l);
			if(writePolicy.equals(WRITE_BACK)){
			if(cache[index][0]!=null){
				if(cache[index][0].isDirty()){
					for(int i= 0;i<s/l;i++){
						Memory.store(cache[index][i].getAddress(), cache[index][i].getValue());
					}
				}
			}
			}
			addToCache(index,temp,value,currentTime);
			l2.updateFromL3Cache(address ,temp, cache[index], currentTime);
			return value[address-temp];
			} else if (m == s / l) {
			for (int i = 0; i < cache.length; i++) {
				if (cache[i][0] == null) {
					addToCache(i,temp,value,currentTime);
					l2.updateFromL3Cache(address ,temp, cache[i], currentTime);
					return value[address-temp];
				}
			}
			int index = LRU();
			if(writePolicy.equals(WRITE_BACK)){
				if(cache[index][0].isDirty()){
					for(int i= 0;i<s/l;i++){
						Memory.store(cache[index][i].getAddress(), cache[index][i].getValue());
					}
				}
			}
			addToCache(index,temp,value,currentTime);
			l2.updateFromL3Cache(address ,temp, cache[index], currentTime);
			return value[address-temp];
			} else {
			baseAddress = (int) (baseAddress / Math.pow(2, disp));
			int index = baseAddress % (s / l / m);
			for (; index < cache.length; index += (s / l / m)) {
				if (cache[index][0] == null) {
					addToCache(index,temp,value,currentTime);
					l2.updateFromL3Cache(address ,temp, cache[index], currentTime);
					return value[address-temp];
				}
			}
			index = baseAddress % (s / l / m);
			int newIndex = setAssociativeLRU(index);
			if(writePolicy.equals(WRITE_BACK)){
				if(cache[newIndex][0].isDirty()){
					for(int i= 0;i<s/l;i++){
						Memory.store(cache[newIndex][i].getAddress(), cache[newIndex][i].getValue());
					}
				}
			}
			addToCache(newIndex,temp,value,currentTime);
			l2.updateFromL3Cache(address ,temp, cache[newIndex], currentTime);
			return value[address-temp];
			}

	}
	/**
	 * Read miss in L3 Cache, which get the instruction from the memory and updates
	 * the L3 Cache (This is called only if one level cache).
	 * 
	 * @param address
	 *            the address of the instruction to be read from the memory.
	 * @param currentTime
	 *            the time at which the operation is performed
	 * @return the instruction read from the memory.
	 */
	public Instruction readMissICache(int address, long currentTime) {
		int baseAddress = (int) (address - address%Math.pow(2, disp)); //The base address is modified in the code below
		Instruction [] instruction = new Instruction[l/16];
		for(int i=0; i<instruction.length; i++)
			instruction[i] = Memory.loadInstructionUsingAbsoluteAddress(baseAddress+i);
		int temp = baseAddress; //Stored in temp value to be used as the original base address
		if (m == 1) {
			int disp = (Log.log(l / 16));
			baseAddress = (int) (baseAddress / Math.pow(2, disp));
			int index = baseAddress % (s / l);
			if(writePolicy.equals(WRITE_BACK)){
			if(cache[index][0]!=null){
				if(cache[index][0].isDirty()){
					for(int i= 0;i<s/l;i++){
						Memory.store(cache[index][i].getAddress(), cache[index][i].getValue());
					}
				}
			}
			}
			addToICache(index,temp,instruction,currentTime);
			l2.updateFromL3ICache(address ,temp, cache[index], currentTime);
			return instruction[address-temp];
			} else if (m == s / l) {
			for (int i = 0; i < cache.length; i++) {
				if (cache[i][0] == null) {
					addToICache(i,temp,instruction,currentTime);
					l1.updateFromL2ICache(address ,baseAddress, cache[i], currentTime);
					return instruction[address-temp];
				}
			}
			int index = LRU();
			if(writePolicy.equals(WRITE_BACK)){
				if(cache[index][0].isDirty()){
					for(int i= 0;i<s/l;i++){
						Memory.store(cache[index][i].getAddress(), cache[index][i].getValue());
					}
				}
			}
			addToICache(index,temp,instruction,currentTime);
			l2.updateFromL3ICache(address ,temp, cache[index], currentTime);
			return instruction[address-temp];
			} else {
			baseAddress = (int) (baseAddress / Math.pow(2, disp));
			int index = baseAddress % (s / l / m);
			for (; index < cache.length; index += (s / l / m)) {
				if (cache[index][0] == null) {
					addToICache(index,temp,instruction,currentTime);
					l2.updateFromL3ICache(address ,temp, cache[index], currentTime);
					return instruction[address-temp];
				}
			}
			index = baseAddress % (s / l / m);
			int newIndex = setAssociativeLRU(index);
			if(writePolicy.equals(WRITE_BACK)){
				if(cache[newIndex][0].isDirty()){
					for(int i= 0;i<s/l;i++){
						Memory.store(cache[newIndex][i].getAddress(), cache[newIndex][i].getValue());
					}
				}
			}
			addToICache(newIndex,temp,instruction,currentTime);
			l2.updateFromL3ICache(address ,temp, cache[newIndex], currentTime);
			return instruction[address-temp];
			}

	}
	
	/**
	 * Gets the index of the least recently used word in certain line in the
	 * cache.
	 * 
	 * @param index
	 *            the line to which the check is made.
	 * @return the place of the word in the line with least recently value.
	 */
	private int setAssociativeLRU(int index) {
        long minSoFar= cache[index][0].getTimeInCache();
        int newIndex = index;
        for(;index<cache.length;index+=(s/l/m)){
        	if(cache[index][0].getTimeInCache()<minSoFar){
        		minSoFar = cache[index][0].getTimeInCache();
        		newIndex = index;
        	}
        }
		return newIndex;
	}
	
	/**
	 * Adds the list of values in the cache. The line contains multiple
	 * words.
	 * 
	 * @param index
	 * @param baseAddress
	 * @param value
	 * @param currentTime
	 */
	private void addToCache(int index, int baseAddress, int[] value, long currentTime) {
		int size = l/16;
		//cache[index] = new CacheEntry[size];
		for(int i=0;i<size;i++){
			cache[index][i] = new CacheEntry(baseAddress+i, value[i], currentTime);
		}
	}
	
	/**
	 * Adds the list of instructions in the cache. The line contains multiple
	 * words.
	 * 
	 * @param index
	 * @param baseAddress
	 * @param value
	 * @param currentTime
	 */
	private void addToICache(int index, int baseAddress, Instruction[] instruction, long currentTime) {
		int size = l/16;
		//cache[index] = new CacheEntry[size];
		for(int i=0;i<size;i++){
			cache[index][i] = new CacheEntry(baseAddress+i, instruction[i], currentTime);
		}
	}
	
	private int LRU() {
		Arrays.sort(cache);
		return 0;
	}


	public int getCycles() {
		return cycles;
	}


	public void setCycles(int cycles) {
		this.cycles = cycles;
	}


	public L1Cache getL1() {
		return l1;
	}


	public void setL1(L1Cache l1) {
		this.l1 = l1;
	}


	public int getMemAddSize() {
		return memAddSize;
	}


	public void setMemAddSize(int memAddSize) {
		this.memAddSize = memAddSize;
	}


	public L2Cache getL2() {
		return l2;
	}


	public void setL2(L2Cache l2) {
		this.l2 = l2;
	}
	
	public String toString(){
		String x = "";
		for(int i = 0; i<cache.length; i++){
			x += "Index: " + i + " ";
			for(int j = 0; j<cache[i].length && cache[i][j] != null; j++){
				if(cache[i][j].getInstruction() == null)
				x +=  "Type: Data " + cache[i][j].toString();
				else
				x +=  "Type: Instruction "  + cache[i][j].getInstruction().shortToString() + " | ";
			}
			x+="\n";
		}
		return x;
	}
	
}
